Skip to content

fix(lint): object/missing-name-field 认 nameField、不再采信已退役的 titleFormat (#6108) - #6338

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-6108-name-field-predicate
Aug 7, 2026
Merged

fix(lint): object/missing-name-field 认 nameField、不再采信已退役的 titleFormat (#6108)#6338
hotlong merged 1 commit into
mainfrom
claude/issue-6108-name-field-predicate

Conversation

@hotlong

@hotlong hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #6108

object/missing-name-field 的谓词从不读 obj.nameField,却仍采信 ADR-0079 已退役的
obj.titleFormat。净效果:照平台自己的迁移建议把 titleFormat 换成 nameField 的对象,
反而多得一条 "records will display as raw IDs" suggestion;守着已退役的键不动的对象反而干净。

1. 前提复核(在 origin/main 上核对过,前提成立)

packages/lint/src/data-model-rules.ts:382-386(合并基 a682670)现行形状,与 issue 正文
dist 读到的完全一致:

    // R9 — object should have a derivable display/primary field.
    const hasNameField =
      !!obj.primaryField ||
      !!obj.titleFormat ||
      fields.some((f) => NAME_LIKE_FIELDS.includes(f.name));

nameField 在整个 packages/lint/src/data-model-rules.ts 中出现 0 次。前提为真。

2. titleFormat 处置的一致性论证(动手前先读了 validate-record-title.ts)

只读对照,未改动该文件本体。同包与 spec 三处现行处置一致地不把 titleFormat 当作标题面:

位置 titleFormat 的现行处置
packages/lint/src/validate-record-title.ts:82-97 每一处非空声明都报 title-format-retired(warning),文案 "titleFormat is retired (ADR-0079) — migrate to nameField"
packages/spec/src/data/display-name.ts:311-318(objectTitleCompleteness) 判定链是 nameField ?? displayNameField ?? 派生,从不读 titleFormat
packages/spec/src/data/object.zod.ts:1563 describe 前缀即 [DEPRECATED → nameField (ADR-0079)],"render-only … the server cannot return or query it"
packages/spec/src/migrations/registry.ts:59-68 迁移条目 object-titleFormat-to-nameField,期望态是「每个带 titleFormat 的对象都声明 nameField

同包另外两条规则也已经只读规范指针:validate-semantic-roles.ts:190
([nameField, primaryField, displayNameField])与 validate-searchable-fields.ts:199
(nameField ?? displayNameField)——两者都不读 titleFormat
data-model-rules.ts 是全仓唯一把 titleFormat 当标题面、且唯一不读 nameField 的消费者。

因此「摘掉 titleFormat ⇒ titleFormat-only 的对象新得一条 missing-name-field」与 ADR-0079
指向一致而非矛盾:这类对象正是迁移目标,validate-record-title 今天已对同一个对象同时报
title-format-retiredtitle-unresolvable(该双报由
packages/lint/src/validate-record-title.test.ts:73 钉住)。修前是两条规则对同一对象给出相反判断,
修后一致。派单里「若现行处置与此推理冲突则停手」的条件未触发。

实测这一翻转的量级:全仓 74 个带 titleFormat 的源文件中,有 22 个系统对象在摘除后失去唯一逃逸口
(全部是 nameField/displayNameField/name-like 字段皆无的复合模板对象,例如
sys-team-member('{user_id} in {team_id}')、sys-record-sharenotification-delivery)。
这 22 个正是 title-unresolvable 今天已经在报的同一批——两条规则从「互相矛盾」收敛到「重复确认」。

3. 谓词 before / after

before:  hasNameField = !!obj.primaryField || !!obj.titleFormat || fields.some(name-like)
after:   hasNameField = !!obj.nameField   || !!obj.primaryField || fields.some(name-like)

primaryField 与 name-like 两支逐字未动。提示文案改为只点名作者真正能声明的面,并新增
fix 提示(见 §7 第 3 条)。

4. hotcrm 对照面 → fixture 映射(以 fixture 复刻,不跨仓)

新增 describe('lintDataModel — object/missing-name-field (ADR-0079 title face)'),
六条断言。核心一条把 issue 正文那张表整个喂进 lintDataModel,断言只剩两个 line-item 被报:

hotcrm 对象 nameField 本 PR fixture 修前 修后
crm_campaign_member member_number 控制面用例 objects[0] 误报 不报
crm_event_attendee attendee_number 控制面用例 objects[1] 误报 不报
crm_contract contract_number 控制面用例 objects[2] + 独立用例 (a) 误报 不报
crm_forecast display_title 控制面用例 objects[3] 误报 不报
crm_opportunity_line_item (无) 控制面用例 objects[4] 真命中 仍报
crm_quote_line_item (无) 控制面用例 objects[5] + 独立用例 (b) 真命中 仍报

控制面断言写成 expect(flagged(objects)).toEqual(['objects[4].fields', 'objects[5].fields'])
——用完整路径列表而不是计数,以免「因为什么都没产生所以断言通过」的空绿。

另外三条:

  • (c) 两支未动路径,彼此隔离取样(crm_forecast_period 只有 primaryField,
    crm_campaign 只有 name-like 的 name;既有那条 primaryField: 'code' 用例里
    code 本身就是 name-like,两支互相遮蔽,故另取样本);
  • (d) titleFormat-only ⇒ 修后告警,断言注释里写明这是刻意翻转而非回归,并指向
    validate-record-title 的双报;
  • 文案断言:提示必须点名 nameField,fix 必须同时出现 ADR-0079titleFormat

5. 反向验证(先申报,后执行,两段对照)

申报(执行前写定):只把谓词表达式回退成 origin/main 形状(primaryField || titleFormat || name-like),
保留全部新断言与新文案 ⇒ 预期 (a) 与控制面用例、(d) 转红,(b)/(c)/文案断言保持绿。

实测(packages/lint 重新 build 后跑 packages/cli 该测试文件):

 × accepts an object whose title face is an explicit nameField 9ms
 × suggests a name field for a titleFormat-only object (retired key is not a title face) 1ms
 × reproduces the hotcrm control surface: 6 objects in, only the 2 line items flagged 3ms

AssertionError: expected true to be false            <- (a) 误报重现
AssertionError: expected false to be true            <- (d) 退役字段仍被采信
AssertionError: expected [ 'objects[0].fields', …(5) ] to deeply equal [ 'objects[4].fields', …(1) ]
                                                      <- 控制面 6 个全报,而非只报 2 个
 Test Files  1 failed (1)
      Tests  3 failed | 48 passed (51)

与申报逐条吻合:三红三绿,红的方向与原因都是预期的那两个。回退后已按字节还原
(git diff 空),并重新 build + 全绿复跑。

6. changeset 级别依据

@objectstack/lint 是发布包 ⇒ 走真 changeset(非 skip 路线),级别 patch,依据仓内既有惯例:
packages/lint 上「新增规则」用 minor(如 .changeset/lint-visibility-bare-identifier-gate.md
action-body-write-set-lint.md),而「既有规则的判定/文案修正」用 patch —— 同类先例
.changeset/body-write-lint-message-driver-truth.md(诊断文案与运行时事实不符)与
.changeset/flow-lint-loop-body-descent.md(既有规则族漏判)。本 PR 属后者:修的是与平台自身
迁移建议相悖的误报,不新增规则、不改规则 id、不改 severity。v17 窗口期,未使用 major。
只声明 @objectstack/lint(@objectstack/cli 侧只有测试文件变动,不发版),与
flow-lint-loop-body-descent.md 的先例一致。

7. 一处相邻事实,已另立单不夹带(#6326)

复核提示文案时实测到:primaryField 根本不是可声明键

ObjectSchema.safeParse({ name: 'probe_obj', label: 'Probe', primaryField: 'code',
                         fields: { code: { type: 'text', label: 'Code' } } });
// => success: false ; issues: [{ code: 'unrecognized_keys', keys: ['primaryField'], path: [] }]
ObjectSchema.create(/* 同上 */);
// => throws: ObjectSchema.create('probe_obj'): unknown key(s) — primaryField.

三点处置,刻意保守:

  1. 谓词里的 primaryField 支保留不动(派单点 (c) 要求行为不变;摘除它有自己的爆炸半径,
    primaryField 不是可声明键(ObjectSchema 以 unrecognized_keys 拒收),但两条 lint 规则与 objectstack-data 技能文档都把它当作合法 name 面 #6326 的事);
  2. 提示文案不再点名它 —— 旧文案 "has no name/title field or primaryField" 等于教作者去写一个
    schema 会硬拒的键,这是「让 AI 写的元数据难写错」这一条上的实打实的坑。新文案只点名
    nameField 与 name-like 字段,并由断言 expect(issue?.message).not.toContain('primaryField') 钉住;
  3. 另立 primaryField 不是可声明键(ObjectSchema 以 unrecognized_keys 拒收),但两条 lint 规则与 objectstack-data 技能文档都把它当作合法 name 面 #6326 记录完整事实(data-model-rules.ts 的死支、validate-semantic-roles.ts:190
    以及 skills/objectstack-data/SKILL.md:1001 那行把 primaryField 写成合法逃逸口的规则表),
    处置方向交分诊。

8. 消费半径清扫

lintDataModel 的消费者:packages/cli/src/commands/lint.ts:405(os lint)。
按规则消费半径而非编辑包清扫 fixture:全仓检索 titleFormat 出现在测试/fixture 的位置,只有
packages/lint/src/validate-record-title.test.ts(另一条规则,未动)与 packages/spec 的迁移登记表。
没有任何 fixture 靠 titleFormat 逃逸本规则,故无需 fixture 三分类处置。
packages/cli/src/lint/score.ts 与本规则 id / 文案零耦合(检索确认)。

9. 门禁 EXIT

门禁 命令 EXIT
规则单测 npx vitest run test/data-model-rules.test.ts --maxWorkers=2(packages/cli) 0 — 51 passed (51)
packages/lint 全量单测 pnpm --workspace-concurrency=2 --filter @objectstack/lint test 0 — 61 files / 1504 tests passed
eslint pnpm lint 0
typecheck pnpm --workspace-concurrency=2 --filter @objectstack/lint --filter @objectstack/cli typecheck 0
控制字节 pnpm check:nul-bytes 0 — 5966 tracked files,无裸控制字节
changeset pnpm check:empty-changeset / check-changeset-no-major.mjs / check-changeset-fixed.mjs 0 / 0 / 0

packages/cli 首次 typecheck 报了 17 条 TS2307 "Cannot find module",全部是新 worktree 里
未 build 的 workspace 依赖(@objectstack/runtime / driver-sql / types …),与本 PR 文件面无关;
pnpm --filter '@objectstack/cli^...' build 后重跑 EXIT 0。

10. 不在本 PR 里


Generated by Claude Code

…#6108)

`object/missing-name-field` 的谓词从不读 `obj.nameField`,却仍采信 `obj.titleFormat`,
于是同一个包里两条规则互相矛盾:`validate-record-title.ts` 把每一处 `titleFormat` 都报成
`title-format-retired` 并按 ADR-0079 指示迁移到 `nameField`,共享判定
`objectTitleCompleteness` 也从不读它——照这条迁移建议做的对象反而多得一条
"records will display as raw IDs" suggestion。下游 hotcrm main 实测 6 命中 4 误报,
四个对象都显式声明了 `nameField`。

- 谓词补读 `nameField`,摘掉 `titleFormat` 一支;`primaryField` 与 name-like 两支不动。
- titleFormat-only 的对象因此新得本规则一条 suggestion:刻意翻转,不是回归——
  这类对象正是 ADR-0079 要迁移的那批,`validate-record-title` 今天已对其同时报
  `title-format-retired` 与 `title-unresolvable`,两条规则从此判定一致。
- 提示文案只点名作者真能声明的面,并新增 `fix` 说明 `titleFormat` 不算标题面。
  旧文案里的 `primaryField` 不再出现:实测 `ObjectSchema.create()` 以
  `unrecognized_keys` 拒收该键(另立 #6326),提示不该广告一个会被 schema 硬拒的键。
- 新增断言复刻 hotcrm 对照面(6 进 2 出),并覆盖 nameField / 无 name 面 /
  primaryField / name-like / titleFormat-only 五条路径。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 7, 2026 2:10pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/releases/v17.mdx (via @objectstack/lint)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants